<template>
  <div class="el-date-editor el-range-editor el-input__inner" :class="[
      'el-date-editor--' + type,
      pickerSize ? `el-range-editor--${ pickerSize }` : '',
      pickerDisabled ? 'is-disabled' : '',
      pickerVisible ? 'is-active' : ''
    ]" @click="handleRangeClick" @mouseenter="handleMouseEnter" @mouseleave="showClose = false"
    @keydown="handleKeydown" ref="reference" v-clickoutside="handleClose">
    <i :class="['el-input__icon', 'el-range__icon', triggerClass]"></i>
    <input autocomplete="off" :placeholder="startPlaceholder" :value="displayValue && displayValue[0]"
      :disabled="pickerDisabled" v-bind="firstInputId" :readonly="!editable || readonly" :name="name && name[0]"
      @input="handleStartInput" @change="handleStartChange" @focus="handleFocus" class="el-range-input">
    <slot name="range-separator">
      <span class="el-range-separator">{{ rangeSeparator }}</span>
    </slot>
    <input autocomplete="off" :placeholder="endPlaceholder" :value="displayValue && displayValue[1]"
      :disabled="pickerDisabled" v-bind="secondInputId" :readonly="!editable || readonly" :name="name && name[1]"
      @input="handleEndInput" @change="handleEndChange" @focus="handleFocus" class="el-range-input">
    <i @click="handleClickIcon" v-if="haveTrigger" :class="[showClose ? '' + clearIcon : '']"
      class="el-input__icon el-range__close-icon">
    </i>
  </div>
</template>

<script>
  import Vue from 'vue';
  import Clickoutside from 'element-ui/src/utils/clickoutside';
  import { formatDate, parseDate, isDateObject, getWeekNumber } from 'element-ui/src/utils/date-util';
  import Popper from 'element-ui/src/utils/vue-popper';
  import Emitter from 'element-ui/src/mixins/emitter';
  import ElInput from 'element-ui/packages/input';
  import merge from 'element-ui/src/utils/merge';

  const NewPopper = {
    props: {
      appendToBody: Popper.props.appendToBody,
      offset: Popper.props.offset,
      boundariesPadding: Popper.props.boundariesPadding,
      arrowOffset: Popper.props.arrowOffset
    },
    methods: Popper.methods,
    data() {
      return merge({ visibleArrow: true }, Popper.data);
    },
    beforeDestroy: Popper.beforeDestroy
  };

  const DEFAULT_FORMATS = {
    date: 'yyyy-MM-dd',
    month: 'yyyy-MM',
    datetime: 'yyyy-MM-dd HH:mm:ss',
    time: 'HH:mm:ss',
    week: 'yyyywWW',
    timerange: 'HH:mm:ss',
    daterange: 'yyyy-MM-dd',
    monthrange: 'yyyy-MM',
    datetimerange: 'yyyy-MM-dd HH:mm:ss',
    year: 'yyyy'
  };
  const HAVE_TRIGGER_TYPES = [
    'date',
    'datetime',
    'time',
    'time-select',
    'week',
    'month',
    'year',
    'daterange',
    'monthrange',
    'timerange',
    'datetimerange',
    'dates'
  ];
  const DATE_FORMATTER = function (value, format) {
    if (format === 'timestamp') return value.getTime();
    return formatDate(value, format);
  };
  const DATE_PARSER = function (text, format) {
    if (format === 'timestamp') return new Date(Number(text));
    return parseDate(text, format);
  };
  const RANGE_FORMATTER = function (value, format) {
    if (Array.isArray(value) && value.length === 2) {
      const start = value[0];
      const end = value[1];

      if (start && end) {
        return [DATE_FORMATTER(start, format), DATE_FORMATTER(end, format)];
      }
    }
    return '';
  };
  const RANGE_PARSER = function (array, format, separator) {
    if (!Array.isArray(array)) {
      array = array.split(separator);
    }
    if (array.length === 2) {
      const range1 = array[0];
      const range2 = array[1];

      return [DATE_PARSER(range1, format), DATE_PARSER(range2, format)];
    }
    return [];
  };
  const TYPE_VALUE_RESOLVER_MAP = {
    default: {
      formatter(value) {
        if (!value) return '';
        return '' + value;
      },
      parser(text) {
        if (text === undefined || text === '') return null;
        return text;
      }
    },
    week: {
      formatter(value, format) {
        let week = getWeekNumber(value);
        let month = value.getMonth();
        const trueDate = new Date(value);
        if (week === 1 && month === 11) {
          trueDate.setHours(0, 0, 0, 0);
          trueDate.setDate(trueDate.getDate() + 3 - (trueDate.getDay() + 6) % 7);
        }
        let date = formatDate(trueDate, format);

        date = /WW/.test(date)
          ? date.replace(/WW/, week < 10 ? '0' + week : week)
          : date.replace(/W/, week);
        return date;
      },
      parser(text, format) {
        // parse as if a normal date
        return TYPE_VALUE_RESOLVER_MAP.date.parser(text, format);
      }
    },
    date: {
      formatter: DATE_FORMATTER,
      parser: DATE_PARSER
    },
    datetime: {
      formatter: DATE_FORMATTER,
      parser: DATE_PARSER
    },
    daterange: {
      formatter: RANGE_FORMATTER,
      parser: RANGE_PARSER
    },
    monthrange: {
      formatter: RANGE_FORMATTER,
      parser: RANGE_PARSER
    },
    datetimerange: {
      formatter: RANGE_FORMATTER,
      parser: RANGE_PARSER
    },
    timerange: {
      formatter: RANGE_FORMATTER,
      parser: RANGE_PARSER
    },
    time: {
      formatter: DATE_FORMATTER,
      parser: DATE_PARSER
    },
    month: {
      formatter: DATE_FORMATTER,
      parser: DATE_PARSER
    },
    year: {
      formatter: DATE_FORMATTER,
      parser: DATE_PARSER
    },
    number: {
      formatter(value) {
        if (!value) return '';
        return '' + value;
      },
      parser(text) {
        let result = Number(text);

        if (!isNaN(text)) {
          return result;
        } else {
          return null;
        }
      }
    },
    dates: {
      formatter(value, format) {
        return value.map(date => DATE_FORMATTER(date, format));
      },
      parser(value, format) {
        return (typeof value === 'string' ? value.split(', ') : value)
          .map(date => date instanceof Date ? date : DATE_PARSER(date, format));
      }
    }
  };
  const PLACEMENT_MAP = {
    left: 'bottom-start',
    center: 'bottom',
    right: 'bottom-end'
  };

  const parseAsFormatAndType = (value, customFormat, type, rangeSeparator = '-') => {
    if (!value) return null;
    const parser = (
      TYPE_VALUE_RESOLVER_MAP[type] ||
      TYPE_VALUE_RESOLVER_MAP['default']
    ).parser;
    const format = customFormat || DEFAULT_FORMATS[type];
    return parser(value, format, rangeSeparator);
  };

  const formatAsFormatAndType = (value, customFormat, type) => {
    if (!value) return null;
    const formatter = (
      TYPE_VALUE_RESOLVER_MAP[type] ||
      TYPE_VALUE_RESOLVER_MAP['default']
    ).formatter;
    const format = customFormat || DEFAULT_FORMATS[type];
    return formatter(value, format);
  };

  /*
   * Considers:
   *   1. Date object
   *   2. date string
   *   3. array of 1 or 2
   */
  const valueEquals = function (a, b) {
    // considers Date object and string
    const dateEquals = function (a, b) {
      const aIsDate = a instanceof Date;
      const bIsDate = b instanceof Date;
      if (aIsDate && bIsDate) {
        return a.getTime() === b.getTime();
      }
      if (!aIsDate && !bIsDate) {
        return a === b;
      }
      return false;
    };

    const aIsArray = a instanceof Array;
    const bIsArray = b instanceof Array;
    if (aIsArray && bIsArray) {
      if (a.length !== b.length) {
        return false;
      }
      return a.every((item, index) => dateEquals(item, b[index]));
    }
    if (!aIsArray && !bIsArray) {
      return dateEquals(a, b);
    }
    return false;
  };

  const isString = function (val) {
    return typeof val === 'string' || val instanceof String;
  };

  const validator = function (val) {
    // either: String, Array of String, null / undefined
    return (
      val === null ||
      val === undefined ||
      isString(val) ||
      (Array.isArray(val) && val.length === 2 && val.every(isString))
    );
  };

  export default {
    mixins: [Emitter, NewPopper],

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },

    props: {
      size: String,
      format: String,
      valueFormat: String,
      readonly: Boolean,
      placeholder: String,
      startPlaceholder: String,
      endPlaceholder: String,
      prefixIcon: String,
      clearIcon: {
        type: String,
        default: 'el-icon-circle-close'
      },
      name: {
        default: '',
        validator
      },
      disabled: Boolean,
      clearable: {
        type: Boolean,
        default: true
      },
      id: {
        default: '',
        validator
      },
      popperClass: String,
      editable: {
        type: Boolean,
        default: true
      },
      align: {
        type: String,
        default: 'left'
      },
      value: {},
      defaultValue: {},
      defaultTime: {},
      rangeSeparator: {
        default: '-'
      },
      pickerOptions: {},
      unlinkPanels: Boolean,
      validateEvent: {
        type: Boolean,
        default: true
      }
    },

    components: { ElInput },

    directives: { Clickoutside },

    data() {
      return {
        pickerVisible: false,
        showClose: false,
        userInput: null,
        valueOnOpen: null, // value when picker opens, used to determine whether to emit change
        unwatchPickerOptions: null
      };
    },

    mounted() {
      this.showPicker();
    },

    watch: {
      pickerVisible(val) {
        if (this.readonly || this.pickerDisabled) return;
        if (val) {
          this.showPicker();
          this.valueOnOpen = Array.isArray(this.value) ? [...this.value] : this.value;
        } else {
          this.hidePicker();
          this.emitChange(this.value);
          this.userInput = null;
          if (this.validateEvent) {
            this.dispatch('ElFormItem', 'el.form.blur');
          }
          this.$emit('blur', this);
          this.blur();
        }
      },
      parsedValue: {
        immediate: true,
        handler(val) {
          if (this.picker) {
            this.picker.value = val;
          }
        }
      },
      defaultValue(val) {
        // NOTE: should eventually move to jsx style picker + panel ?
        if (this.picker) {
          this.picker.defaultValue = val;
        }
      },
      value(val, oldVal) {
        if (!valueEquals(val, oldVal) && !this.pickerVisible && this.validateEvent) {
          this.dispatch('ElFormItem', 'el.form.change', val);
        }
      }
    },

    computed: {
      ranged() {
        return this.type.indexOf('range') > -1;
      },

      reference() {
        const reference = this.$refs.reference;
        return reference.$el || reference;
      },

      refInput() {
        if (this.reference) {
          return [].slice.call(this.reference.querySelectorAll('input'));
        }
        return [];
      },

      valueIsEmpty() {
        const val = this.value;
        if (Array.isArray(val)) {
          for (let i = 0, len = val.length; i < len; i++) {
            if (val[i]) {
              return false;
            }
          }
        } else {
          if (val) {
            return false;
          }
        }
        return true;
      },

      triggerClass() {
        return this.prefixIcon || (this.type.indexOf('time') !== -1 ? 'el-icon-time' : 'el-icon-date');
      },

      selectionMode() {
        if (this.type === 'week') {
          return 'week';
        } else if (this.type === 'month') {
          return 'month';
        } else if (this.type === 'year') {
          return 'year';
        } else if (this.type === 'dates') {
          return 'dates';
        }

        return 'day';
      },

      haveTrigger() {
        if (typeof this.showTrigger !== 'undefined') {
          return this.showTrigger;
        }
        return HAVE_TRIGGER_TYPES.indexOf(this.type) !== -1;
      },

      displayValue() {
        const formattedValue = formatAsFormatAndType(this.parsedValue, this.format, this.type, this.rangeSeparator);
        if (Array.isArray(this.userInput)) {
          return [
            this.userInput[0] || (formattedValue && formattedValue[0]) || '',
            this.userInput[1] || (formattedValue && formattedValue[1]) || ''
          ];
        } else if (this.userInput !== null) {
          return this.userInput;
        } else if (formattedValue) {
          return this.type === 'dates'
            ? formattedValue.join(', ')
            : formattedValue;
        } else {
          return '';
        }
      },

      parsedValue() {
        if (!this.value) return this.value; // component value is not set
        if (this.type === 'time-select') return this.value; // time-select does not require parsing, this might change in next major version

        const valueIsDateObject = isDateObject(this.value) || (Array.isArray(this.value) && this.value.every(isDateObject));
        if (valueIsDateObject) {
          return this.value;
        }

        if (this.valueFormat) {
          return parseAsFormatAndType(this.value, this.valueFormat, this.type, this.rangeSeparator) || this.value;
        }

        // NOTE: deal with common but incorrect usage, should remove in next major version
        // user might provide string / timestamp without value-format, coerce them into date (or array of date)
        return Array.isArray(this.value) ? this.value.map(val => new Date(val)) : new Date(this.value);
      },

      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },

      pickerSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },

      pickerDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      },

      firstInputId() {
        const obj = {};
        let id;
        if (this.ranged) {
          id = this.id && this.id[0];
        } else {
          id = this.id;
        }
        if (id) obj.id = id;
        return obj;
      },

      secondInputId() {
        const obj = {};
        let id;
        if (this.ranged) {
          id = this.id && this.id[1];
        }
        if (id) obj.id = id;
        return obj;
      }
    },

    created() {
      // vue-popper
      this.popperOptions = {
        boundariesPadding: 0,
        gpuAcceleration: false
      };
      this.placement = PLACEMENT_MAP[this.align] || PLACEMENT_MAP.left;

      this.$on('fieldReset', this.handleFieldReset);
    },

    methods: {
      focus() {
        if (!this.ranged) {
          this.$refs.reference.focus();
        } else {
          this.handleFocus();
        }
      },

      blur() {
        this.refInput.forEach(input => input.blur());
      },

      // {parse, formatTo} Value deals maps component value with internal Date
      parseValue(value) {
        const isParsed = isDateObject(value) || (Array.isArray(value) && value.every(isDateObject));
        if (this.valueFormat && !isParsed) {
          return parseAsFormatAndType(value, this.valueFormat, this.type, this.rangeSeparator) || value;
        } else {
          return value;
        }
      },

      formatToValue(date) {
        const isFormattable = isDateObject(date) || (Array.isArray(date) && date.every(isDateObject));
        if (this.valueFormat && isFormattable) {
          return formatAsFormatAndType(date, this.valueFormat, this.type, this.rangeSeparator);
        } else {
          return date;
        }
      },

      // {parse, formatTo} String deals with user input
      parseString(value) {
        const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
        return parseAsFormatAndType(value, this.format, type);
      },

      formatToString(value) {
        const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
        return formatAsFormatAndType(value, this.format, type);
      },

      handleMouseEnter() {
        if (this.readonly || this.pickerDisabled) return;
        if (!this.valueIsEmpty && this.clearable) {
          this.showClose = true;
        }
      },

      handleChange() {
        if (this.userInput) {
          const value = this.parseString(this.displayValue);
          if (value) {
            this.picker.value = value;
            if (this.isValidValue(value)) {
              this.emitInput(value);
              this.userInput = null;
            }
          }
        }
        if (this.userInput === '') {
          this.emitInput(null);
          this.emitChange(null);
          this.userInput = null;
        }
      },

      handleStartInput(event) {
        if (this.userInput) {
          this.userInput = [event.target.value, this.userInput[1]];
        } else {
          this.userInput = [event.target.value, null];
        }
      },

      handleEndInput(event) {
        if (this.userInput) {
          this.userInput = [this.userInput[0], event.target.value];
        } else {
          this.userInput = [null, event.target.value];
        }
      },

      handleStartChange(event) {
        const value = this.parseString(this.userInput && this.userInput[0]);
        if (value) {
          this.userInput = [this.formatToString(value), this.displayValue[1]];
          const newValue = [value, this.picker.value && this.picker.value[1]];
          this.picker.value = newValue;
          if (this.isValidValue(newValue)) {
            this.emitInput(newValue);
            this.userInput = null;
          }
        }
      },

      handleEndChange(event) {
        const value = this.parseString(this.userInput && this.userInput[1]);
        if (value) {
          this.userInput = [this.displayValue[0], this.formatToString(value)];
          const newValue = [this.picker.value && this.picker.value[0], value];
          this.picker.value = newValue;
          if (this.isValidValue(newValue)) {
            this.emitInput(newValue);
            this.userInput = null;
          }
        }
      },

      handleClickIcon(event) {
        if (this.readonly || this.pickerDisabled) return;
        if (this.showClose) {
          this.valueOnOpen = this.value;
          event.stopPropagation();
          this.emitInput(null);
          this.emitChange(null);
          this.showClose = false;
          if (this.picker && typeof this.picker.handleClear === 'function') {
            this.picker.handleClear();
          }
        } else {
          // this.pickerVisible = !this.pickerVisible;
        }
      },

      handleClose() {
        if (!this.pickerVisible) return;
        // this.pickerVisible = false;

        if (this.type === 'dates') {
          // restore to former value
          const oldValue = parseAsFormatAndType(this.valueOnOpen, this.valueFormat, this.type, this.rangeSeparator) || this.valueOnOpen;
          this.emitInput(oldValue);
        }
      },

      handleFieldReset(initialValue) {
        this.userInput = initialValue === '' ? null : initialValue;
      },

      handleFocus() {
        const type = this.type;

        if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
          // this.pickerVisible = true;
        }
        this.$emit('focus', this);
      },

      handleKeydown(event) {
        const keyCode = event.keyCode;

        // ESC
        if (keyCode === 27) {
          // this.pickerVisible = false;
          event.stopPropagation();
          return;
        }

        // Tab
        if (keyCode === 9) {
          if (!this.ranged) {
            this.handleChange();
            // this.pickerVisible = this.picker.visible = false;
            this.blur();
            event.stopPropagation();
          } else {
            // user may change focus between two input
            setTimeout(() => {
              if (this.refInput.indexOf(document.activeElement) === -1) {
                // this.pickerVisible = false;
                this.blur();
                event.stopPropagation();
              }
            }, 0);
          }
          return;
        }

        // Enter
        if (keyCode === 13) {
          if (this.userInput === '' || this.isValidValue(this.parseString(this.displayValue))) {
            this.handleChange();
            // this.pickerVisible = this.picker.visible = false;
            this.blur();
          }
          event.stopPropagation();
          return;
        }

        // if user is typing, do not let picker handle key input
        if (this.userInput) {
          event.stopPropagation();
          return;
        }

        // delegate other keys to panel
        if (this.picker && this.picker.handleKeydown) {
          this.picker.handleKeydown(event);
        }
      },

      handleRangeClick() {
        const type = this.type;

        if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
          this.pickerVisible = true;
        }
        this.$emit('focus', this);
      },

      hidePicker() {
        if (this.picker) {
          this.picker.resetView && this.picker.resetView();
          // this.pickerVisible = this.picker.visible = false;
          this.destroyPopper();
        }
      },

      showPicker() {
        if (this.$isServer) return;
        if (!this.picker) {
          this.mountPicker();
        }
        this.pickerVisible = this.picker.visible = true;

        this.updatePopper();

        this.picker.value = this.parsedValue;
        this.picker.resetView && this.picker.resetView();

        this.$nextTick(() => {
          this.picker.adjustSpinners && this.picker.adjustSpinners();
        });
      },

      mountPicker() {
        this.picker = new Vue(this.panel).$mount();
        this.picker.defaultValue = this.defaultValue;
        this.picker.defaultTime = this.defaultTime;
        this.picker.popperClass = this.popperClass;
        this.popperElm = this.picker.$el;
        this.picker.width = this.reference.getBoundingClientRect().width;
        this.picker.showTime = this.type === 'datetime' || this.type === 'datetimerange';
        this.picker.selectionMode = this.selectionMode;
        this.picker.unlinkPanels = this.unlinkPanels;
        this.picker.arrowControl = this.arrowControl || this.timeArrowControl || false;
        this.$watch('format', (format) => {
          this.picker.format = format;
        });

        const updateOptions = () => {
          const options = this.pickerOptions;

          if (options && options.selectableRange) {
            let ranges = options.selectableRange;
            const parser = TYPE_VALUE_RESOLVER_MAP.datetimerange.parser;
            const format = DEFAULT_FORMATS.timerange;

            ranges = Array.isArray(ranges) ? ranges : [ranges];
            this.picker.selectableRange = ranges.map(range => parser(range, format, this.rangeSeparator));
          }

          for (const option in options) {
            if (options.hasOwnProperty(option) &&
              // 忽略 time-picker 的该配置项
              option !== 'selectableRange') {
              this.picker[option] = options[option];
            }
          }

          // main format must prevail over undocumented pickerOptions.format
          if (this.format) {
            this.picker.format = this.format;
          }
        };
        updateOptions();
        this.unwatchPickerOptions = this.$watch('pickerOptions', () => updateOptions(), { deep: true });
        this.$el.appendChild(this.picker.$el);
        this.picker.resetView && this.picker.resetView();

        this.picker.$on('dodestroy', this.doDestroy);
        this.picker.$on('pick', (date = '', visible = false) => {
          this.userInput = null;
          // this.pickerVisible = this.picker.visible = visible;
          this.emitInput(date);
          this.picker.resetView && this.picker.resetView();
        });

        this.picker.$on('select-range', (start, end, pos) => {
          if (this.refInput.length === 0) return;
          if (!pos || pos === 'min') {
            this.refInput[0].setSelectionRange(start, end);
            this.refInput[0].focus();
          } else if (pos === 'max') {
            this.refInput[1].setSelectionRange(start, end);
            this.refInput[1].focus();
          }
        });
      },

      unmountPicker() {
        if (this.picker) {
          this.picker.$destroy();
          this.picker.$off();
          if (typeof this.unwatchPickerOptions === 'function') {
            this.unwatchPickerOptions();
          }
          this.picker.$el.parentNode.removeChild(this.picker.$el);
        }
      },

      emitChange(val) {
        // determine user real change only
        if (!valueEquals(val, this.valueOnOpen)) {
          this.$emit('change', val);
          this.valueOnOpen = val;
          if (this.validateEvent) {
            this.dispatch('ElFormItem', 'el.form.change', val);
          }
        }
      },

      emitInput(val) {
        const formatted = this.formatToValue(val);
        if (!valueEquals(this.value, formatted)) {
          this.$emit('input', formatted);
        }
      },

      isValidValue(value) {
        if (!this.picker) {
          this.mountPicker();
        }
        if (this.picker.isValidValue) {
          return value && this.picker.isValidValue(value);
        } else {
          return true;
        }
      }
    }
  };
</script>